# vim: set et sta ts=4 sw=4 sts=4:
# vi: set ts=4:

package Utils;

use strict;
use warnings;
use Smart::Comments -ENV, '###';

use POSIX;
use File::Spec::Functions;
use File::Basename;
use File::Path qw(make_path);
use IPC::Open3;
use IO::Select;
use Carp qw(cluck);
use Net::DNS;

use constant LOG_DIR => '/var/log/ccamr';
use constant PID_DIR => '/var/run/ccamr';

use base qw(Exporter);
our @EXPORT = qw(
    runlog dbglog buglog errlog warnlog crash
    modifiedSince lastModifyTime inodeOf
    firstLineOf getHostName getDeviceID
    extendVar extendSize extendTime dig lcpLen
    getEnabledApps getAppList
    timeRun killProcessTree makePath
    LOG_DIR PID_DIR
);

$|=1;

sub modifiedSince {
    my ($file, $time, $delay) = @_;
    $delay ||= 5;

    my $modify = (stat $file)[9] or return;

    # $delay 秒之内修改的文件暂时不处理
    if ( time() - $modify < $delay ){
        return 0;
    }

    if ( $modify > $time ){
        return 1;
    }

    return 0;
}

sub lastModifyTime {
    my $file = shift or return;

    my $time = (stat $file)[9];

    return $time;
}

sub inodeOf {
    my $path = shift;

    my $inode = (stat $path)[1];

    return $inode;
}

sub logmsg {
    my ($type, $tag, @info) = @_;

    my $now = strftime "%Y-%m-%d %H:%M:%S", localtime;
    $type = pack 'A3', $type;
    $tag = pack 'A3', $tag;

    my $allInfo = join ' ', @info;
    foreach my $line ( split /\r?\n/, $allInfo ){
        print "$type $tag $now $line\n";
    }
}

sub dbglog {
    logmsg( DBG => @_ );
}

sub buglog {
    logmsg( DBG => @_ );
    cluck( "以上 BUG 被发现于" );
}

sub runlog {
    logmsg( RUN => @_ );
}

sub errlog {
    logmsg( ERR => @_ );
}

sub warnlog {
    logmsg( WRN => @_ );
}

sub crash {
    logmsg( SYS => @_ );
    exit 1;
}

###############################################################
# 功能: 取文件第一行的内容，用这个函数可以实现许多功能
#
sub firstLineOf {
    my $file = shift;

    open my $fh, $file or return '';
    my $line = <$fh>;
    chomp $line;
    $line =~ s/^\s+|\s+$//g;

    return $line;
}

###############################################################
# 功能: 取设备名
#
sub getHostName {
    my $hostname = (uname)[1];
    return $hostname;
}

###############################################################
# 功能: 取设备ID
#
sub getDeviceID {
    my $deviceID = firstLineOf( '/sn.txt' ) || 'Unknown';
    return $deviceID;
}

#######################################################################
# 功能: 将带有单位的时间转换成以秒为单位的数字
# 参数: 带单位的时间
#
sub extendTime {
    my $value = shift || 0;

    if ( $value =~ /^(\d+)s?$/ ){
        $value = $1;
    }
    elsif( $value =~ /^(\d+)m$/ ){
        $value = $1 * 60;
    }
    elsif( $value =~ /^(\d+)h$/ ){
        $value = $1 * 60 * 60;
    }
    elsif( $value =~ /^(\d+)d$/ ){
        $value = $1 * 60 * 60 * 24;
    }
    else{
        logmsg( "时间间隔错误。" );
        $value = 60;
    }

    return int($value);
}

#######################################################################
# 功能: 将 Unix 纪元的时间转换成人类可读的时间字符串
# 参数: 时间戳
#
sub makeHumanTime {
    my $time;

    return sprintf "%s (Unix Time: %d)", scalar localtime($time), $time;
}

#######################################################################
# 功能: 创建 PID 文件
# 参数: 文件名, PID
#
sub createPidFile {
    my ($file, $pid) = @_;

    # 将进程号写入 PID 文件
    open my $fh, '>', catfile( PID_DIR, "$file.pid" ) or die;
    print $fh "$pid\n";
    close $fh;
}


sub getWholeFile {
    my $file = shift;
    open my $fh, '<', $file or return;
    binmode $fh;
    my @line = <$fh>;
    if ( wantarray ){
        return @line;
    }
    else{
        return join '', @line;
    }
}

sub getNicIP {
    my $nic = shift;

    my $line = `ifconfig $nic | grep 'inet addr'`;
    my ($ip) = $line =~ /inet addr:(\S+)/;

    return $ip;
}

sub extendSize {
    my $value = shift;

    if ( $value =~ /[Bb]$/ ){
        return $`;
    }
    elsif ( $value =~ /[Kk]$/ ){
        return $` * 1024;
    }
    elsif ( $value =~ /[Mm]$/ ){
        return $` * 1024 * 1024;
    }
    elsif ( $value =~ /[Gg]$/ ){
        return $` * 1024 * 1024 * 1024;
    }

    return int($value);
}

sub extendVar {
    my $str = shift;
    my $vars = shift || {};

    my $now = time();
    my $timeStamp = strftime "%Y%m%d%H%M%S", localtime($now);
    my $hostName = getHostName();
    my $deviceID = getDeviceID();

    $str =~ s/{TIME}/$timeStamp/ige;
    $str =~ s/{HOSTNAME}/$hostName/ige;
    $str =~ s/{DEVICEID}/$deviceID/ige;

    foreach my $key ( keys %$vars ){
        my $value = $vars->{$key};
        $str =~ s/\Q{$key}\E/$value/ige;
    }

    return $str;
}

sub uptime {
    my $line = firstLineOf( '/proc/uptime' );

    my ($uptime, $idle) = split /\s+/, $line;

    return $uptime;
}

=item B<lcpLen>

输入: 两个 hostname

说明: 计算这两个 hostname 的最大公共前缀的长度: 0 - ISP 都不同；1 - 仅 ISP 相同；2 - ISP-城市 相同；3 - ISP-城市-小节点 相同；4 - 就是同一台机器。

=cut
sub lcpLen {
    my ($left, $right) = @_;

    # 主机名格式不对
    my $format = qr/^[A-Z]{3}-[A-Z]{2}-[A-Z0-9]-[a-zA-Z0-9]{3}$/;
    if ( $left !~ $format or $right !~ $format ){
        return 0;
    }

    if ( substr( $left, 0, 3 ) ne substr( $right, 0, 3 ) ){
        return 0;
    }

    if ( substr( $left, 0, 6 ) ne substr( $right, 0, 6 ) ){
        return 1;
    }

    if ( substr( $left, 0, 8 ) ne substr( $right, 0, 8 ) ){
        return 2;
    }

    if ( $left ne $right ){
        return 3;
    }

    return 4;
}

=item B<dig>

输入: $domain

说明: 解析域名。警告：本函数不是 POE 事件，它是阻塞运行的。

=cut
sub dig {
    my $domain = shift;

    if ( $domain =~ /^\d+\.\d+\.\d+\.\d+$/ ){
        return $domain;
    }

    my $res = new Net::DNS::Resolver(
        nameservers => [qw(
            202.108.251.50
            211.147.212.130
            211.94.66.252
            222.73.41.218
            58.68.141.200
        )]
    );

    my $query = $res->query( $domain, 'A' );
    if ( not $query ){
        errlog( DNS => "DNS 解析失败:", $res->errorstring );
        return;
    }

    my $ip;
    foreach my $rr ( $query->answer ) {
        next unless $rr->type eq 'A';
        next unless $rr->name eq $domain;
        next unless defined $rr->address;
        $ip = $rr->address;
        runlog( DNS => "DNS 解析成功: $domain => $ip" );
    }

    if ( not defined $ip ){
        errlog( DNS => "DNS 解析失败: $domain 域名没有返回任何 IP。" );
        return;
    }

    return $ip;
}

=item B<timeRun>

输入: $cmdLine, $timeout

说明: 在规定的时间内运行命令

=cut
sub timeRun {
    my $cmd = shift;
    my $timeout = shift || 60;
    my $finalTime = time + $timeout;

    if ( $timeout <= 0 ){
        $finalTime = undef;
    }

    $SIG{PIPE} = 'IGNORE';

    my ($in, $out, $output);
    my $pid = eval { open3( $in, $out, $out, $cmd ) };

    if ( not $pid ){
        return ( error => "执行命令错误: $!" );
    }

    my $s = new IO::Select( $out );
    while(1){
        my @ready = $s->can_read(1);
        if ( @ready ){
            my $line = <$out>;
            if ( defined $line ){
                $output .= $line;
            }
        }

        my $kid = waitpid( $pid, WNOHANG );
        if ( $kid > 0 ){
            if ( $? == -1 ){
                errlog( CMD => "执行 $cmd 命令失败: $!" );
            }
            elsif ( $? & 127 ) {
                my $msg = sprintf "执行 $cmd 命令时遇到信号 %d, %s coredump 文件。",
                   ( $? & 127 ), ( $? & 128 ) ? '生成了' : '没有生成';
                errlog( CMD => $msg );
            }
            elsif ( $? != 0 ) {
                errlog( CMD => "命令 $cmd 的返回码是 $?。" );
            }

            last;
        }

        if ( defined $finalTime and time >= $finalTime ){
            killProcessTree( $pid );
            return ( timeout => "命令执行超时。" );
        }
    }

    return ( ok => $output );
}

sub killProcessTree {
    my $which = shift;

    return unless -f "/proc/$which/status";

    my $map;
    while ( my $dir = glob "/proc/[0-9]*" ){
        next unless -d $dir and -f "$dir/status";
        open my $fh, '<', "$dir/status" or next;
        my %info = map { chomp; split /:\s+/, $_, 2 } <$fh>;
        my ($tgid, $pid, $ppid) = @info{qw(Tgid Pid PPid)};
        next if $pid != $tgid;  # skip thread
        $map->{$pid} = {};
        push @{ $map->{$ppid}->{childs} }, $pid;
    }

    return unless exists $map->{$which};

    my $getPids = sub {
        my ($map, $pid, $sub) = @_;
        my @list;

        push @list, $pid;
        foreach my $child ( @{ $map->{$pid}->{childs} || [] } ){
            push @list, $sub->( $map, $child, $sub );
        }

        return @list;
    };

    my @pids = $getPids->( $map, $which, $getPids );

    kill TERM => @pids;
    select undef, undef, undef, 0.25;
    kill KILL => @pids;

    do{ select undef, undef, undef, 0.25 } while waitpid( $which, WNOHANG ) != $which;
}

sub makePath {
    my $path = shift;

    my $err;
    make_path( $path, { error => \$err } );
    for my $diag ( @$err ) {
        my ($file, $message) = %$diag;
        chomp $message;
        if ( $file eq '' ) {
            errlog( MKD => "创建目录 $path 时遇到错误: $message" );
        }
        else {
            errlog( MKD => "创建目录 $file 时遇到错误: $message" );
        }
    }
}

1;
